﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Data.SqlClient;
using NCommet.Core;
using System.Configuration;
using System.Reflection;
using NCommet.Core.Services;
using log4net;
using System.Threading;
using NCommet.Core.Configuration;
using System.Data.SqlTypes;
using NCommet.Core.Agents;

namespace NCommet.Modules.DBSorter
{
    /// <summary>
    /// An enumeration of the supported datatypes for indexing and sorting.
    /// </summary>
    public enum SupportedTypes
    {
        String = 1,
        DateTime = 2,
        Int = 3
    }

    class IdleQueue<T>
    {
        private ILog MyLog { get { return LogManager.GetLogger("DBIndexer"); } }
        private Queue<T> q = new Queue<T>();
        private object monitor = new object();
        private DateTime lastPush = DateTime.MaxValue;

        public IdleQueue(TimeSpan offset)
        {
            this.offset = offset;
        }

        private TimeSpan offset;

        public TimeSpan Offset
        {
            get { return offset; }
            set { offset = value; }
        }

        public void Enqueue(T item)
        {
            lock (monitor)
            {
                lastPush = DateTime.Now.Add(offset);
                if (!q.Contains(item))
                    q.Enqueue(item);
                else
                    MyLog.Debug("Already in-queue, ignoring");
            }
        }

        public T Dequeue()
        {
            lock (monitor)
            {
                return q.Dequeue();
            }
        }

        public bool Empty
        {
            get
            {
                lock (monitor)
                {
                    return q.Count <= 0;
                }
            }
        }

        public int Count
        {
            get
            {
                lock (monitor)
                {
                    return q.Count;
                }
            }
        }

        public bool ReadyToDequeue()
        {
            lock (monitor)
            {
                return q.Count > 0 && lastPush < DateTime.Now;
            }
        }
    }


    public class DBIndexer : NCommet.Core.Agents.IModule
    {
        private ILog MyLog { get { return LogManager.GetLogger("DBIndexer"); } }

        private IdleQueue<int> q = new IdleQueue<int>(TimeSpan.FromSeconds(10));

        /// <summary>
        /// Push an item id in the queue
        /// </summary>
        /// <param name="ItemId"></param>
        private void Push(int ItemId)
        {
            MyLog.DebugFormat("Push {0}", ItemId);
            q.Enqueue(ItemId);
        }

        void Run()
        {
            try
            {
                while (true)
                {
                    while (!q.ReadyToDequeue())
                    {
                        Thread.Sleep(1000);
                    }
                    if (!q.Empty)
                    {
                        int itemId = q.Dequeue();
                        MyLog.DebugFormat("Will process {0}", itemId);

                        NCommetContainer.Instance.Persister.RequestExplicitSession();

                        Item toCheck = Item.Get(itemId, true); // override security because we're in another thread and there is no HttpContext here

                        // delete index
                        DeleteIndex(itemId);

                        if (toCheck != null)
                            GenerateIndex(toCheck);
                    }
                }
            }
            catch (Exception ex)
            {
                MyLog.Error("Error", ex);
            }
        }



        private void StartThread()
        {
            Thread thread = new Thread(new ThreadStart(Run));
            thread.Start();
        }

        public DBIndexer()
        { }

        /// <summary>
        /// Initializes module by subscribing to EventSink ItemSaved and ItemDeleted events.
        /// </summary>
        public void InitModule()
        {
            StartThread();
            EventSink.Instance.ItemSaved += new ItemHandler(Instance_ItemSaved);
            EventSink.Instance.ItemDeleted += new ItemHandler(Instance_ItemDeleted);
        }

        bool disabled = false;

        #region EventSink event handlers

        void Instance_ItemDeleted(object sender, ItemEventArgs e)
        {
            if (!disabled)
                Push(e.Item.Id);
        }

        void Instance_ItemSaved(object sender, ItemEventArgs e)
        {
            if (disabled || e == null || e.Item == null || e.Item.Id <= 0)
                return;
            Push(e.Item.Id);
        }
        #endregion

        private bool IsTypeSupported(Type t)
        {
            return (t == typeof(string) || t == typeof(DateTime) || t == typeof(int));
        }

        private SupportedTypes GetSupportedType(Type t)
        {
            if (t == typeof(string))
                return SupportedTypes.String;
            else if (t == typeof(DateTime))
                return SupportedTypes.DateTime;
            else if (t == typeof(int))
                return SupportedTypes.Int;
            else
                throw new ArgumentException("Type " + t + " is not supported");
        }

        private void GenerateIndex(Item item)
        {
            MyLog.DebugFormat("GenerateIndex for {0}", item.Id);
            ContentBase content = item.Content;
            if (content == null)
                return;
            Type t = content.GetType();
            PropertyInfo[] properties = t.GetProperties();
            foreach (PropertyInfo p in properties)
            {
                if (!p.CanRead)
                    continue; // what's the point if I can't even read it

                if (!p.IsDefined(typeof(IndexableAttribute), false))
                    continue; // it's not marked as indexable

                object[] attrs = p.GetCustomAttributes(typeof(IndexableAttribute), false);
                if (attrs == null || attrs.Length != 1)
                    continue; // something went bad
                IndexableAttribute ia = attrs[0] as IndexableAttribute;
                if (ia == null)
                    continue; // something went really bad

                if (!IsTypeSupported(p.PropertyType))
                    throw new Exception("Property is not currently supported");

                // we finally found the sort key
                int sortKey = ia.Key;
                SupportedTypes? stCatalog = GetSupportedTypeFromCatalog(sortKey);
                SupportedTypes stReal = GetSupportedType(p.PropertyType);

                if (stCatalog.HasValue && stCatalog.Value != stReal)
                    throw new Exception("Invalid property type. Catalog property is " + stCatalog + ", actual property was " + stReal);

                if (!stCatalog.HasValue)
                    SetSupportedTypeInCatalog(sortKey, stReal);


                object value = p.GetGetMethod().Invoke(content, null);

                MyLog.DebugFormat("ItemId={0}, property={1}, value={2}", item.Id, p.Name, value);

                if (value is int)
                    InsertIndex(item, sortKey, (int)value);
                else if (value is DateTime)
                    InsertIndex(item, sortKey, (DateTime)value);
                else
                    InsertIndex(item, sortKey, Convert.ToString(value));
            }
        }

        #region SQL Data Access
        private static SqlConnection GetConnection()
        {
            ConnectionStringSettings css = ConfigurationManager.ConnectionStrings["MySqlConnection"];
            if (css == null)
                throw new Exception("Connection not declared");
            if (string.IsNullOrEmpty(css.ConnectionString))
                throw new Exception("Empty connection string");
            return new SqlConnection(css.ConnectionString);
        }

        internal static SupportedTypes? GetSupportedTypeFromCatalog(int sortKey)
        {
            using (SqlConnection sql = GetConnection())
            {
                sql.Open();
                using (SqlCommand cmd = sql.CreateCommand())
                {
                    cmd.CommandText = "SELECT DataType FROM ItemIndexCatalog WHERE SortKey=@sortKey";
                    cmd.Parameters.Add("sortKey", System.Data.SqlDbType.Int).Value = sortKey;
                    using (SqlDataReader dr = cmd.ExecuteReader())
                    {
                        while (dr.Read())
                        {
                            return (SupportedTypes)dr.GetInt32(0);
                        }
                        return null;
                    }
                }
            }
        }

        private void SetSupportedTypeInCatalog(int sortKey, SupportedTypes supportedType)
        {
            using (SqlConnection sql = GetConnection())
            {
                sql.Open();
                using (SqlCommand cmd = sql.CreateCommand())
                {
                    cmd.CommandText = "INSERT INTO ItemIndexCatalog (SortKey,DataType) VALUES (@sortKey,@dataType)";
                    cmd.Parameters.Add("sortKey", System.Data.SqlDbType.Int).Value = sortKey;
                    cmd.Parameters.Add("dataType", System.Data.SqlDbType.Int).Value = (int)supportedType;
                    cmd.ExecuteNonQuery();
                }
            }
        }


        private void DeleteIndex(int id)
        {
            using (SqlConnection sql = GetConnection())
            {
                sql.Open();
                SqlCommand cmd = sql.CreateCommand();
                cmd.CommandText = "DELETE FROM ItemIndex WHERE ID=@id";
                cmd.Parameters.Add("id", System.Data.SqlDbType.Int).Value = id;
                cmd.ExecuteNonQuery();
            }            
        }

        private void InsertIndex(Item item, int sortKey, string sortValue)
        {
            using (SqlConnection sql = GetConnection())
            {
                sql.Open();
                using (SqlCommand cmd = sql.CreateCommand())
                {
                    cmd.CommandText = "INSERT INTO ItemIndex (ID,SortKey,SortValueStr) VALUES (@id,@sortKey,@sortValue)";
                    cmd.Parameters.Add("id", System.Data.SqlDbType.Int).Value = item.Id;
                    cmd.Parameters.Add("sortKey", System.Data.SqlDbType.Int).Value = sortKey;
                    cmd.Parameters.Add("sortValue", System.Data.SqlDbType.NVarChar).Value = sortValue;
                    cmd.ExecuteNonQuery();
                }
            }
        }

        private void InsertIndex(Item item, int sortKey, DateTime sortValue)
        {
            if (sortValue < SqlDateTime.MinValue.Value)
                sortValue = SqlDateTime.MinValue.Value;
            else if (sortValue > SqlDateTime.MaxValue.Value)
                sortValue = SqlDateTime.MaxValue.Value;

            using (SqlConnection sql = GetConnection())
            {
                sql.Open();
                using (SqlCommand cmd = sql.CreateCommand())
                {
                    cmd.CommandText = "INSERT INTO ItemIndex (ID,SortKey,SortValueDate) VALUES (@id,@sortKey,@sortValue)";
                    cmd.Parameters.Add("id", System.Data.SqlDbType.Int).Value = item.Id;
                    cmd.Parameters.Add("sortKey", System.Data.SqlDbType.Int).Value = sortKey;
                    cmd.Parameters.Add("sortValue", System.Data.SqlDbType.DateTime).Value = sortValue;
                    cmd.ExecuteNonQuery();
                }
            }
        }

        private void InsertIndex(Item item, int sortKey, int sortValue)
        {
            using (SqlConnection sql = GetConnection())
            {
                sql.Open();
                using (SqlCommand cmd = sql.CreateCommand())
                {
                    cmd.CommandText = "INSERT INTO ItemIndex (ID,SortKey,SortValueInt) VALUES (@id,@sortKey,@sortValue)";
                    cmd.Parameters.Add("id", System.Data.SqlDbType.Int).Value = item.Id;
                    cmd.Parameters.Add("sortKey", System.Data.SqlDbType.Int).Value = sortKey;
                    cmd.Parameters.Add("sortValue", System.Data.SqlDbType.Int).Value = sortValue;
                    cmd.ExecuteNonQuery();
                }
            }
        }
        #endregion

    }
}
